可以將陣列視為能夠裝載任何值的容器,不論是number , string , object,甚至是另一個陣列(產生多維陣列)。
無需為陣列先預設大小,它會自動維護length的值。
請避免建立「稀疏(sparse)」陣列,使陣列產生空插槽(empty slots)。
稀疏陣列不會發生錯誤,但請避免這樣的方式。
稀疏陣列的a[1]
顯示undefined,但這跟我們明確指定a[1] = undefined
還是有所不同。
陣列最大的特點就是以數值來索引的,但陣列也是物件,所以它也擁有物件的行為「string鍵值(keys)/屬性(properties)」。
如若使用string鍵值,length將不列入計算。
如果,鍵值能夠被強制轉型為十進位的number,那JS就會將之當作number索引使用,而非string鍵值。
雖然陣列可以使用string鍵值(keys)/屬性(properties),然而,陣列最大的特點就是使用number作為索引。
與其如此,不如直接使用object就好。
以數值做索引的集合,但非真正的陣列。
例如,DOM的查詢,都會回傳由DOM元素所組成的串列(list),但非陣列。
如果我們要進行轉換,可使用ES6的新功能Array.form()
,會在ES6筆記另行介紹。
注意,這觀點修正了筆者之前對字串(string)的認知
一般的認知是「string是字元(characters)所組成的陣列」,表面看似如此,但實際上有些行為卻是大不相同。
以上的結果都一樣,似乎string就是字元(characters)所組成的陣列。
但,不盡然如此。
字串的"o"依然是小寫,但陣列卻改變了,這正是字串與陣列最大的差異點。
JS中的字串是不可變(immutable)的,而陣列是可變(mutable)的。
另外,a[1]這種方式,並非洽當,較正確的作法是a.charAt(1)
。
不可變的意思是,字串的內容無法就地(in-place)修正,而是會回傳一個新的字串。
可變的意思是,陣列的內容可以就地修正。
我們也可以使用不會更動內容的陣列方法join( )
以及map( )
。
因為目標為字串,所以我們藉由Function.prototype.call
來呼叫陣列方法。
反轉字串(reversing string)
字串並沒有像陣列一樣擁有反轉方法reverse( )
。
因為字串是不可變的,所以也無法使用Array.prototype.reverse.call(a)
,會擲出錯誤。
變通就是將字串轉為陣列,再反轉,再轉為字串。
如果我們對於字串的處理較常當作陣列的對待的話,比較好的方式是直接把字串儲存為陣列,而非字串,省去轉來轉的去的麻煩,若需要使用字串表示,再使用join()
方法轉成字串。
數值(number)
JS中只有一種數值型別「number」。包含整數以及帶有小數點的十進位數字。
JS並沒有真正的整數,89跟89.0一樣都是整數。
JS number的實作是基於「IEEE754」標準的「雙精度(double precision)」格式(64位元的二進位數字)。
數值語法
數字字面值通常以10為基底(base-10)表示。
小數點之後的0會被移除。
非常大或非常小的數值預設會以指數形式輸出,與toExponential()
方法輸出的格式相同。
toFixed()
方法能夠指定使用幾個小數位數。
注意,輸出的型別為字串,若要求超過數值本身的位數,會以0補齊。
toPrecision()
方法指定的是多少位的有效數字(significant digits)
也可以不透過變數,直接在數字字面值上處理,不過這種方式相當少見。
也可以使用指數形式來表示很大的字面值。
小數值的陷阱
從數學上來說0.1+0.2=0.3是絕對成立的,但為何答案是false?
原因就在於IEEE754的二進位浮點數表示並不是完全100%的精確,還是有極小的誤差。
所以,當處理小數值的時候,要特別注意這種情況,但大部分處理整數的時候,JS的數值表示是安全的。
如果真的要比較0.1+0.2與0.3的話,我們可以使用「約整誤差(rounding error)」值作為比較誤差的容許值(2^-52 (2.220446049250313e-16) )。
ES6的Number.EPSILON
已經預先定義該容許值
所以我們可以這樣使用:
Number.MAX_VALUE
,能夠表示的最大浮點數。Number.MIN_VALUE
,能夠表示的最小浮點數。
JS中number值的表示,有一個安全範圍(所請求的值都可以精確地表示)
ES6中已預先定義安全值。
Number.MAX_SAFE_INTEGER
,能夠表示的最大安全值。Number.MIN_SAFE_INTEGER
,能夠表示的最小安全值。
JS遇到大數字的情況大多是處理資料庫的64位元ID,此類的值無法以number精確地表示出來,所以在JS中都以string儲存或傳輸。
若要測試某個值是否為整數,可使用ES6規格的Number.isInteger( )
。
若要測試某個值是否為安全整數(safe integer),可使用ES6規格的Number.isSafeInteger( )
。
undefined型別只有「undefined」值。
null型別只有「null」值。
undefined與null的差異:
或是
null是一個關鍵字(keyword),而非識別字(identifier),意味著它不能是變數。
undefined是識別字,在非strict模式中,可以指定值給undefined識別字,但非常不建議這樣做。
執行數學運算,若無法產生一個有效值,此時就會得到NaN值。
NaN按照字面上解釋,代表「not a number」(不是一個數字)。但更為洽當的解釋應該將之視為「無效的數字」、「不合格的數字」或是「壞掉的數字」更為適合。
既然已經明確指出a為NaN,但它的型別卻是number,這是令人比較困惑的地方。
發生NaN可以解釋的情況是「我們嘗試做數學運算,但失敗了,所以得到一個失敗的number結果」。
如果我們想要確認變數是否為NaN值的話,使用下列方式會導致失敗。
NaN有個特別之處就是,它永遠都不等於自己。
可以使用isNaN( )
來測試。
問題看似解決了,卻還是有缺陷。
"foo"的確不是number,但它也不是NaN。會造成這結果主要是因為isNaN( )
太過依賴於NaN這個字面上的意義了。
ES6提供了一個解決方案,Number.isNaN( )
方法,我們還可以利用它永遠都不等於自己的特性來判斷。
雖然看似奇怪,但的確可行。
在傳統的程式語言執行除以0的數學運算,會發生除以0的錯誤。
但在JS中,並不會發生錯誤,而是會產Infinity/-Infinity值。
ES6以經預先定義了無限值。
JS使用的是有限的數值表示法,所以像是數學運算的結果,有可能會產生溢位(overflow)。
如果運算結果產生了超出顯示範圍的值,就會依據IEEE754的「約整至最接近值(round-to-nearest)」的規範來決定顯示結果。
a + Math.pow( 2, 969 )
的結果比較接近Number.MAX_VALUE,而非Infinity,所以會「向下約整(rounds down)」,顯示Number.MAX_VALUE。
a + Math.pow( 2, 969 )
的結果比較接近Infinity,所以會「向上約整(rounds up)」,顯示Infinity。
從數學角度來看,無限除以無限是一個無法定義的運算,所以,Infinity/Infinity的結果會是NaN。
若把任何有限的number除以Infinity,那結果會是0。
ES6新增一個測試特殊值相等性的方法,Object.is( )
方法。
它可以測試NaN與-0的值是否相等。
值可藉由值的拷貝(value-copy)或參考的拷貝(reference-copy)來指定或傳遞。
在JS中,沒有指標(pointers)這種機制,無法從某個變數指向另一個變數的參考。
在JS中,值的型別完全決定該值是藉由值的拷貝(by value-copy)或參考的拷貝(by reference-copy)來指定的。
純量的基型值(primitives)永遠都是藉由值的拷貝(by value-copy)來指定或傳遞的:null、undefined、string、number、boolean。
複合值的object(array、封裝的物件包裹器boxed object wrappers、與function),永遠都是藉由參考的拷貝(by reference-copy)來指定或傳遞的。
以上面的例子來說,a的值是2,為純量的基型值(primitives),b就會被指定該值的另一份拷貝,變更b時,不會動到a的值。
c和d是指向[1,2,3]的個別參考,兩者所參考的都是同一個[1,2,3],當透過其中一個參考來更動array時,影響到的是唯一共有值[1,2,3],而兩者之後都會參考到經過修改的新值[1,2,3,4]。
一開始a跟b都指向同一個值[1,2,3],之後我們將b的參考指向另一個值[4,5,6],這並不會影響到a的參考。
這時,a跟b個別指向不同的值。
function foo(x) {
x.push( 4 );
x; //[1,2,3,4]
x = [4,5,6];
x.push( 7 );
x; //[4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; //[1,2,3,4]
當我們將a
傳給foo( )
時,它會將a
的參考拷貝給x
,所以a
跟x
是指向同一個值([1,2,3]
)的不同參考。接下來,我們使用push( 4 )
來變更值。
但是,執行x = [4,5,6]
的時候,我們只是把x
的參考指向[4,5,6]
,a
的參考並無變動。
所以,我們無法透過x
的參考來改變a
要指向哪個值。我們只能修改a
跟x
所指向共有值的內容。
如若要讓a
持有[4,5,6,7]
,就必須修改a
所指向的值的內容。
function foo(x) {
x.push( 4 );
x; //[1,2,3,4]
x.length = 0;
x.push( 4, 5, 6, 7 );
x; //[4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; //[4,5,6,7]
執行x.length = 0
及x.push( 4, 5, 6, 7 )
會直接修改共有的array,所以a
自然就會持有[4,5,6,7]
。
注意,我們無法控制值拷貝(by value-copy)或參考拷貝(by reference-copy)的行為,一切都由值的型別來決定。
如果我們希望,array的值,能夠有值拷貝(by value-copy)行為的話,就必須透過slice( )
方法,它能複製出所指向值的淺層拷貝,以上面的例子來說,foo( a.slice() )
,所傳入的是藉由slice( )
製作出來的a參考的拷貝,並非a的參考,因此不會影響a所參考的值。
如果,我們希望基型值(primitives)能有參考拷貝(by reference-copy)的行為該怎麼做?
那就得將值包在能有有參考拷貝(by reference-copy)行為的型別(object、array)之中了。
function foo(wrapper) {
wrapper.a = 42;
}
var obj = {
a: 2
};
foo( obj );
obj.a; // 42
物件obj的屬性a值是基型值(primitives),將obj傳入foo( ),行為就像是參考拷貝(by reference-copy),透過wrapper.a的指定,也會影響obj.a。
JS並沒有指標,不會指向其他變數或參考,只會指向底層的值。
此為You Don't Know JS系列的筆記。